/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2022. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * Description:
 * Author: huawei
 * Create: 2019-10-15
 */

#include <linux/sched.h>

#include "dbl/uda.h"
#include "devmm_common.h"
#include "svm_hot_reset.h"

#define DEVMM_IS_READY 1

STATIC struct svm_business_info *g_svm_business_info = NULL;

int devmm_alloc_business_info(void)
{
    u32 i;

    g_svm_business_info = devmm_kvzalloc(sizeof(struct svm_business_info));
    if (g_svm_business_info == NULL) {
        devmm_drv_err("Devmm_kvzalloc svm_business_info fail.\n");
        return -ENOMEM;
    }

    for (i = 0; i < DEVMM_MAX_DEVICE_NUM; i++) {
        init_rwsem(&g_svm_business_info->business_ref_cnt_rw_sema[i]);
        INIT_LIST_HEAD(&g_svm_business_info->pid_info[i].list);
    }
    init_rwsem(&g_svm_business_info->stop_business_rw_sema);

    return 0;
}

void devmm_free_business_info(void)
{
    if (g_svm_business_info != NULL) {
        devmm_kvfree(g_svm_business_info);
        g_svm_business_info = NULL;
    }
}

static bool devmm_get_stop_business_flag(u32 devid)
{
    return (uda_is_phy_dev(devid) == true) ?
        ((g_svm_business_info->stop_business_flag & (1UL << devid)) != 0) : false;
}

static void devmm_set_stop_business_flag(u32 devid)
{
    if (uda_is_phy_dev(devid) == true) {
        down_write(&g_svm_business_info->stop_business_rw_sema);
        g_svm_business_info->stop_business_flag |= 1 << devid;
        up_write(&g_svm_business_info->stop_business_rw_sema);
    }
}

static void devmm_clear_stop_business_flag(u32 devid)
{
    if (uda_is_phy_dev(devid) == true) {
        down_write(&g_svm_business_info->stop_business_rw_sema);
        g_svm_business_info->stop_business_flag &= ~(1 << devid);
        up_write(&g_svm_business_info->stop_business_rw_sema);
    }
}

STATIC int devmm_add_pid_to_business_inner(u32 devid, pid_t pid)
{
    struct svm_business_pid_info *business_pid_info_of_list = NULL;
    struct svm_business_pid_info *business_pid_info = NULL;
    struct list_head *head = NULL;
    struct list_head *pos = NULL;
    struct list_head *n = NULL;

    business_pid_info = devmm_kvzalloc(sizeof(struct svm_business_pid_info));
    if (business_pid_info == NULL) {
        devmm_drv_err("Devmm_kvzalloc svm business info failed.\n");
        return -ENOMEM;
    }

    down_write(&g_svm_business_info->business_ref_cnt_rw_sema[devid]);
    /* Traverse the list to check whether the PID exists. If the PID does not exist, add it. Otherwise, exit. */
    head = &g_svm_business_info->pid_info[devid].list;
    list_for_each_safe(pos, n, head) {
        business_pid_info_of_list = list_entry(pos, struct svm_business_pid_info, list);
        if (business_pid_info_of_list->pid == pid) {
            goto existed_pid_info;
        }
    }

    business_pid_info->pid = pid;
    list_add(&business_pid_info->list, head);
    g_svm_business_info->business_ref_cnt[devid]++;
    up_write(&g_svm_business_info->business_ref_cnt_rw_sema[devid]);
    devmm_drv_info("Add current pid info to svm_business_info. (devid=%u; pid=%d)\n", devid, pid);
    return 0;
existed_pid_info:
    up_write(&g_svm_business_info->business_ref_cnt_rw_sema[devid]);
    devmm_kvfree(business_pid_info);
    return 0;
}

int devmm_add_pid_to_business(u32 devid, pid_t pid)
{
    int ret;

    down_read(&g_svm_business_info->stop_business_rw_sema);
    if (devmm_get_stop_business_flag(devid)) {
        up_read(&g_svm_business_info->stop_business_rw_sema);
        devmm_drv_err("Hotreset stop flag has been set, the process will return. (devid=%u; pid=%d)\n",
                      devid, pid);
        return -EBUSY;
    }

    ret = devmm_add_pid_to_business_inner(devid, pid);
    up_read(&g_svm_business_info->stop_business_rw_sema);
    if (ret != 0) {
        devmm_drv_err("Add current pid info to svm_business_info failed. (devid=%u; pid=%d)\n", devid, pid);
    }

    return ret;
}

void devmm_remove_pid_from_business(pid_t pid)
{
    struct svm_business_pid_info *business_pid_info = NULL;
    struct list_head *head = NULL;
    struct list_head *pos = NULL;
    struct list_head *n = NULL;
    int pid_node_deleted = 0;
    int dev_id;

    for (dev_id = 0; dev_id < DEVMM_MAX_DEVICE_NUM; dev_id++) {
        down_write(&g_svm_business_info->business_ref_cnt_rw_sema[dev_id]);
        /* Traverse the list node corresponding to the PID and delete the node. */
        head = &g_svm_business_info->pid_info[dev_id].list;
        list_for_each_safe(pos, n, head) {
            business_pid_info = list_entry(pos, struct svm_business_pid_info, list);
            if (business_pid_info->pid == pid) {
                list_del(pos);
                g_svm_business_info->business_ref_cnt[dev_id]--;
                pid_node_deleted = 1;
                devmm_drv_info(
                    "Remove pid info from svm_business_info. (devid=%u; pid=%d; business_refcount=%u)\n",
                    dev_id, pid, g_svm_business_info->business_ref_cnt[dev_id]);
                break;
            }
        }
        up_write(&g_svm_business_info->business_ref_cnt_rw_sema[dev_id]);

        if (pid_node_deleted == 1 && business_pid_info != NULL) {
            devmm_kvfree(business_pid_info);
            pid_node_deleted = 0;
            business_pid_info = NULL;
        }
    }
}

void devmm_svm_business_info_init(u32 devid)
{
    /* Restore the hot reset flag of the device that has completed the hot reset. */
    devmm_clear_stop_business_flag(devid);

    /* set bit is ready */
    atomic_set(&g_svm_business_info->devmm_is_ready[devid], DEVMM_IS_READY);

    devmm_drv_info(
        "Restore stop business flag when pcie probe callback svm init_instance. (devid=%u)\n", devid);
    return;
}

void devmm_svm_business_info_uninit(u32 devid)
{
    devmm_set_stop_business_flag(devid);

    /* clear bit of ready */
    atomic_set(&g_svm_business_info->devmm_is_ready[devid], 0);

    devmm_drv_info("Set stop business flag when pcie remove callback svm uninit_instance. (devid=%u)\n", devid);
    return;
}

int devmm_hotreset_pre_handle(u32 dev_id)
{
    u32 business_ref_cnt;

    if (atomic_read(&g_svm_business_info->devmm_is_ready[dev_id]) == 0) {
        devmm_drv_err("Devmm is not ready. (devid=%u)\n", dev_id);
        return -EBUSY;
    }

    devmm_set_stop_business_flag(dev_id);
    down_read(&g_svm_business_info->business_ref_cnt_rw_sema[dev_id]);
    business_ref_cnt = g_svm_business_info->business_ref_cnt[dev_id];
    up_read(&g_svm_business_info->business_ref_cnt_rw_sema[dev_id]);
    if (business_ref_cnt != 0) {
        devmm_clear_stop_business_flag(dev_id);
        devmm_drv_err("Business count of device is not 0. (devid=%u; business_ref_cnt=%u)\n", dev_id, business_ref_cnt);
        return -EBUSY;
    }

    devmm_drv_info("No devmm business running in device. (devid=%u)\n", dev_id);
    return 0;
}

int devmm_hotreset_cancel_handle(u32 dev_id)
{
    devmm_clear_stop_business_flag(dev_id);
    devmm_drv_info("Hotreset stop business flag is canceled. (devid=%u)\n", dev_id);
    return 0;
}
